Skip to content

fix(approval): tell the agent how it can clear the gate itself - #134

Open
Arakiss wants to merge 4 commits into
mainfrom
fix/picto-hint-names-agent-path
Open

fix(approval): tell the agent how it can clear the gate itself#134
Arakiss wants to merge 4 commits into
mainfrom
fix/picto-hint-names-agent-path

Conversation

@Arakiss

@Arakiss Arakiss commented Aug 4, 2026

Copy link
Copy Markdown
Owner

The defect

An ask_picto told the blocked caller to run gommage approval approve <id> — a command reserved for the operator. Agents act on the string they receive at the moment they are blocked, in preference to any doctrine given earlier, so every ask became a handoff to a human.

Measured on one host over 2026-07-27..2026-08-04:

count
asks raised 118
pictos actually created 5
requests left unresolved 83
stale requests closed by one sweep 730

Why the message can safely name self-service

A scope with no input binding is self-serviceable by design — the picto is a signed, audited declaration of intent, not a second password. Rules that must stay operator-only either carry no required_scope at all, or bind the picto to the exact call. Both are respected: when bind_input is set, the message offers the operator command and nothing else.

Before / after

Before:

… require a picto; approval request apr_ace9 pending; run `gommage approval approve apr_ace9`

After:

… require a picto; to proceed yourself: `gommage grant --scope net.out.post --reason "<why>"`
then retry the same call. Operator alternative: `gommage approval approve apr_ace9` (request apr_ace9 pending)

One string, three copies

The message was duplicated in the CLI, the daemon and the MCP server. That is why changing the CLI had no visible effect: the daemon answers the hook. It now lives once in gommage-core::approval_reason and all three call it.

Verification

  • End to end against a copy of a real policy: the same call that produced the operator-only text now returns the self-service path.
  • cargo fmt --all --check clean, cargo clippy --all-targets -D warnings clean, 68 tests pass.
  • Two CLI approval tests asserted the old wording and now assert the new contract (request id present and the self-service path named).
  • beta_check_accepts_public_fixture_library fails identically on the base branch; untouched here.

https://claude.ai/code/session_013tXB1yCznMnJYbDCa7uGrz

Arakiss added 3 commits July 16, 2026 21:11
git push origin :main deletes the remote branch. The bash-git-push mapper
captured the whole refspec, so that command emitted git.push:refs/heads/:main,
which matches no branch gate. The result was that deleting main was allowed
outright while an ordinary push to the same branch required a picto.

Adds a mapper for the empty-source refspec that emits the destination branch,
so existing branch gates apply, plus a git.push.delete capability. The new
gate-remote-branch-delete rule covers every branch rather than only protected
ones: deleting a colleague's feature branch destroys shared work even where
pushing to it is allowed.

Verified against a copy of a real policy tree: git push origin :main, :dev and
:feat/algo all move from allow to ask_picto, while git push origin main,
git push --force origin main and git push origin feat/x keep their previous
decisions. Two regression fixtures cover the deletion cases.
An `ask_picto` told the blocked caller to run `gommage approval approve
<id>` — a command reserved for the operator. Agents act on the string they
receive at the moment they are blocked, in preference to any doctrine given
earlier, so the result was a handoff to a human on every ask. Measured on one
host over 2026-07-27..2026-08-04: 118 asks produced 5 pictos and left 83
requests unresolved, until a sweep closed 730 stale ones in a single pass.

A scope with no input binding is self-serviceable by design: the picto is a
signed, audited declaration of intent, not a second password. The message now
names `gommage grant --scope <scope>` first and keeps the operator command as
the alternative. When the picto IS bound to the exact call, only the operator
can clear it, and the message says exactly that and nothing else.

The string lived in three copies — CLI, daemon and MCP — which is why the
daemon kept answering with the old text after the CLI was changed. It now
lives once in gommage-core and all three call it.

Verified end to end against a copy of a real policy: the same call that
produced the operator-only text now returns the self-service path. fmt and
clippy -D warnings clean; 68 tests pass. `beta_check_accepts_public_fixture_
library` fails identically on the base branch and is untouched by this change.

Claude-Session: https://claude.ai/code/session_013tXB1yCznMnJYbDCa7uGrz
@Arakiss
Arakiss force-pushed the fix/picto-hint-names-agent-path branch from 6d115b2 to aefde79 Compare August 4, 2026 14:54
`beta_check_accepts_public_fixture_library` asserted "8 passed, 0 failed"
against examples/policy-fixtures.yaml. The library grew to ten cases and the
assertion was not updated, so the test failed on every branch — including
untouched ones — and blamed whoever pushed next.

It now asserts what has to hold: the fixture check runs, passes, and reports
zero failures. Adding a case to the public library no longer breaks an
unrelated branch. The failure message dumps the checks array so the next
person does not have to reproduce it locally to see why.

Claude-Session: https://claude.ai/code/session_013tXB1yCznMnJYbDCa7uGrz
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant